package simpleTaskDispatcher.model;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;

import simpleTaskDispatcher.model.enums.DispatchingEvent;
import simpleTaskDispatcher.model.taskDispatchers.ITaskQueueDispatcher;
import simpleTaskDispatcher.model.taskDispatchers.TaskQueueComparators;
import simpleTaskDispatcher.model.taskDispatchers.impl.Dispatchers;
import simpleTaskDispatcher.model.tasks.impl.ReleaseWorkerTask;

public class WorkersPool {

	private BlockingQueue<Worker> freeWorkers = new LinkedBlockingQueue<Worker>();
	
	List<Worker> allWorkers = new ArrayList<Worker>();

	private List<TaskQueue> managedTaskQueues = new LinkedList<TaskQueue>();

	private ITaskQueueDispatcher dispatcher = Dispatchers.giveEqualResource(2, TaskQueueComparators.compareByAscCreation());

	private List<PlannedAssignation> plannedAssignations = new LinkedList<PlannedAssignation>();

	private Semaphore terminatedWorkers = new Semaphore(0);
	
	private boolean closeSignaled;
	
	private String status;
	
	public WorkersPool() {
		onNewWorkerPool(this);
	}
	
	
	public synchronized WorkersPool createWorkers(int nbWorkers) throws InterruptedException {
		assertAlived();
		for (int i = 0; i < nbWorkers; i++) {
			Worker worker = new Worker(this, terminatedWorkers);
			worker.start();
			allWorkers.add(worker);
		}
		doDispatching(DispatchingEvent.NEW_WORKER);
		return this;
	}
	
	public synchronized WorkersPool createWorkerForEachCPU() {
		try {
			createWorkers(Runtime.getRuntime().availableProcessors());
		} catch (InterruptedException e) {
			throw new RuntimeException(e);
		}
		return this;
	}
	
	private void assertAlived() {
		if (closeSignaled) {
			throw new IllegalStateException("This action cannot be done a closed worker pool");
		}
	}
	
	public void closeWorkerPool() throws InterruptedException {
		closeSignaled = true;

		List<TaskQueue> managedTaskQueuesCopy = new ArrayList<TaskQueue>();
		synchronized (managedTaskQueues) {
			managedTaskQueuesCopy.addAll(managedTaskQueues);
		}
		
		for(TaskQueue queue : managedTaskQueuesCopy) {
			queue.close(true);
		}
		
		for(Worker worker : allWorkers) {
			
			if (worker.isWorkerAlive()) {
				worker.close();
			}
			
		}
		
		if (true) {
			terminatedWorkers.acquire(allWorkers.size());
			terminatedWorkers.release(allWorkers.size());
		}
		onCloseWorkerPool(this);
	}
	
	public synchronized void doDispatching(Map<String, ?> extraDispatchParameters)
			throws InterruptedException {
		doDispatching(DispatchingEvent.REQUESTED_BY_USER,
				extraDispatchParameters);
	}
	
	public synchronized void doDispatching()
			throws InterruptedException {
		doDispatching(DispatchingEvent.REQUESTED_BY_USER,
				new HashMap<String, Object>());
	}
	
	public synchronized int freeWorkersSize() {
		int size =  freeWorkers.size();
		return size;
	}

	void release(Worker worker, DispatchingEvent event)
			throws InterruptedException {

		if (closeSignaled) {
			worker.setStatus("Pool closing signaled");
			if (worker.isWorkerAlive()) {
				worker.setStatus("is alived");
				worker.close();
				
				worker.setStatus("is closed");
			}
			
			
			
		} else {
			worker.setStatus("Trying to assignate to an other task queue");
			freeWorkers.put(worker);
			tryAssignate();
			
			//A null event means the release was done because of a dispatch event
			//A NEW_WORKER event is handled directly in the createWorker method
			if (event != DispatchingEvent.NEW_WORKER && plannedAssignations.isEmpty() && !freeWorkers.isEmpty()) {
				worker.setStatus("Require dispatching");
				doDispatching(event);
				worker.setStatus("Dispathcing done");
			}
		}
		worker.setStatus("Releases");
	}


	synchronized void doDispatching(DispatchingEvent event) throws InterruptedException {
		doDispatching(event, new HashMap<String, Object>());
	}
	
	synchronized void doDispatching(DispatchingEvent event,
			Map<String, ?> extraDispatchParameters) throws InterruptedException {
		
		//TODO Do some minimum/maximum validations
		
		while(doDispatchingRound(event));
		tryAssignate();
	}
	
	private void setStatus(String status) {
		this.status	= status;
		//System.out.println(status);
	}

	List<DispatchingChange> changes = new ArrayList<DispatchingChange>();
	public boolean doDispatchingRound(DispatchingEvent event) throws InterruptedException {
		setStatus("dispatchingRound");
		synchronized (managedTaskQueues) {
			cleanTaskQueues();
			
			int plannedFreeWorkers = allWorkers.size();
			for(TaskQueue tf : managedTaskQueues) {
				plannedFreeWorkers -= tf.getPlannedNbWorkers();
			}
			
			dispatcher.dispatch(changes, event == null ? DispatchingEvent.UNKNOWN : event, managedTaskQueues,
					freeWorkers.size(), plannedFreeWorkers, null);
		}
		
		setStatus("dispatchingRound - task queues cleaned");
		boolean dispatchErrors = false;
		for (DispatchingChange change : changes) {
			if (change.from != null) {
				for (int i = 0; i < change.workers; i++) {
					change.from.decrementNbWorkers();
					change.from.putInPriority(new ReleaseWorkerTask(false, null));
				}
			}
			if (change.to != null) {
				if (change.to.isClosed()) {
					dispatchErrors = true;
				} else {                    
					dispatchWorkersTo(change.to, change.workers);	
				}
				
			}
		}
		
		changes.clear();
		
		setStatus("dispatchingRound ended");
		return dispatchErrors;
	}

	public void cleanTaskQueues() {
		Iterator<TaskQueue> managedTaskQueuesIterator = managedTaskQueues.iterator();
		while(managedTaskQueuesIterator.hasNext()) {
			if (managedTaskQueuesIterator.next().isClosed()) {
				managedTaskQueuesIterator.remove();
			}
		}
	}
	
	private synchronized void dispatchWorkersTo(TaskQueue queue, int nbWorkers) {
		PlannedAssignation p = new PlannedAssignation();
		p.taskQueue = queue;
		p.nbWorkers = nbWorkers;
		plannedAssignations.add(p);
	}

	public synchronized void tryAssignate() {
		while (!freeWorkers.isEmpty() && !plannedAssignations.isEmpty()) {
			
			PlannedAssignation p = plannedAssignations.get(0);
			
			int minimumToBeAdded = p.taskQueue.getMinimalWorkers() - p.taskQueue.getPlannedNbWorkers();
			int maximumToBeAdded = p.taskQueue.getMaximalWorkers() - p.taskQueue.getPlannedNbWorkers(); 
					
			if (minimumToBeAdded > freeWorkers.size()) {
				//Not enaugh workers, waiting
				break;
			} else if (maximumToBeAdded == 0) {
				plannedAssignations.remove(0);
				continue;
			} else {
				int addWorkers = Math.min(freeWorkers.size(), Math.min(p.nbWorkers, maximumToBeAdded));
				List<Worker> workers = new ArrayList<Worker>();
				for(int i = 0; i < addWorkers; i++) {
					workers.add(freeWorkers.poll());
				}
				
				p.taskQueue.assignate(workers);
				if (addWorkers < p.nbWorkers) {
					p.nbWorkers -= addWorkers;
				} else {
					plannedAssignations.remove(0);
					continue;
				}
			}
			
		}
		
	}
	
	public void addTaskQueue(TaskQueue taskQueue) throws InterruptedException {
		assertAlived();
		
		synchronized (managedTaskQueues) {
			if (managedTaskQueues.contains(taskQueue)) {
				throw new IllegalStateException("");
			}
			managedTaskQueues.add(taskQueue);
			
			taskQueue.setPool(this);
		}
		
		doDispatching(DispatchingEvent.NEW_TASK_FEED);
		
	}
	
	public void cancelTaskQueue(TaskQueue taskQueue, boolean blocking) throws InterruptedException {
		taskQueue.cancel(blocking);
	}
	
	public synchronized float getProgression() throws InterruptedException {
		synchronized (managedTaskQueues) {
			return (float) (getRealisedWorkValue() / getTotalWorkValue());
		}
	}
	
	public synchronized double getUnrealisedWorkValue() throws InterruptedException {
		synchronized (managedTaskQueues) {
			double total = 0.0f;
			for(TaskQueue taskQueue : managedTaskQueues) {
				total += taskQueue.getUnrealisedWork();
			}
			return total;
		}

	}

	public synchronized double getTotalWorkValue() {
		synchronized (managedTaskQueues) {
			double total = 0.0f;
			for(TaskQueue taskQueue : managedTaskQueues) {
				total += taskQueue.getTotalWorkValue();
			}
			return total;
		}
	}
	
	public synchronized double getRealisedWorkValue() {
		synchronized (managedTaskQueues) {
			double total = 0.0f;
			for(TaskQueue taskQueue : managedTaskQueues) {
				total += taskQueue.getRealisedWork();
			}
			return total;
		}
	}
	
	public boolean isClosed() throws InterruptedException {
		if (!closeSignaled) {
			return false;
		}
		synchronized (managedTaskQueues) {
			cleanTaskQueues();
			boolean closed = managedTaskQueues.isEmpty();
			return closed;
		}
	
	}

	public void setDispatcher(ITaskQueueDispatcher dispatcher) {
		this.dispatcher = dispatcher;
	}
	
	@Override
	public String toString() {
		return status;
	}
	
	private static List<WorkersPool> pools = new ArrayList<WorkersPool>();
	private static synchronized void onNewWorkerPool(WorkersPool pool) {
		pools.add(pool);
	}
	
	private static synchronized void onCloseWorkerPool(WorkersPool pool) {
		pools.remove(pool);
	}
	
	public static synchronized List<WorkersPool> getAllRunningWorkerPools() {
		List<WorkersPool> copy = new ArrayList<WorkersPool>();
		copy.addAll(pools);
		return copy;
	}
}
